package cron

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"
)

var (
	lastDays     = []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
	monthAliases = []string{"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"}
	weekAliases  = []string{"sun", "mon", "tue", "wed", "thu", "fri", "sat"}
	minuteParser = simpleValueParser{0, 59}
	hourParser   = simpleValueParser{0, 23}
	dayParser    = dayOfMonthValueParser{simpleValueParser{min: 1, max: 31}}
	monthParser  = monthValueParser{simpleValueParser{1, 12}}
	weekParser   = weekValueParser{simpleValueParser{0, 7}}
)

type valueParser interface {
	parse(val string) (int, error)
}

type valueMatcher interface {
	match(val int) bool
}

type alwaysTrueValueMatcher struct {
}

func (*alwaysTrueValueMatcher) match(val int) bool {
	return true
}

type intArrayValueMatcher struct {
	values []int
}

func (m *intArrayValueMatcher) add(val int) {
	m.values = append(m.values, val)
}

func (m *intArrayValueMatcher) match(val int) bool {
	for _, v := range m.values {

		if val == v {
			return true
		}
	}
	return false
}

type simpleValueParser struct {
	min int
	max int
}

func (p *simpleValueParser) parse(s string) (int, error) {
	val, err := strconv.Atoi(s)
	if err != nil {
		return -1, err
	}

	if val > p.max || val < p.min {
		return -1, errors.New("value out of range")
	}
	return val, nil
}

type dayOfMonthValueParser struct {
	simpleValueParser
}

func (p *dayOfMonthValueParser) parse(s string) (int, error) {
	if s == "L" || s == "l" {
		return 32, nil
	}
	return p.simpleValueParser.parse(s)
}

type monthValueParser struct {
	simpleValueParser
}

func (p *monthValueParser) parse(s string) (int, error) {
	result, err := p.simpleValueParser.parse(s)
	if err == nil {
		return result, nil
	}
	return parseAlias(s, monthAliases, 1)
}

type weekValueParser struct {
	simpleValueParser
}

func (p *weekValueParser) parse(s string) (int, error) {
	result, err := p.simpleValueParser.parse(s)
	if err == nil {
		return result, nil
	}
	return parseAlias(s, weekAliases, 0)
}

type Cron interface {
	Match(time time.Time) bool
}

func NewCron(exp string) (Cron, error) {

	c := &CronPattern{}
	c.exp = exp

	parts := split(exp, '|')
	for _, p := range parts {

		subs := strings.Fields(p)
		size := len(subs)
		if size < 5 || size > 6 {
			return c, errors.New("invalid pattern")
		}

		if size == 5 {
			subs = append([]string{"*"}, subs...)
		}
		matcher, err := buildCronMatcher(subs)
		if err != nil {
			return c, err
		}

		c.matcher = append(c.matcher, *matcher)

	}
	return c, nil
}

func buildCronMatcher(exp []string) (*cronMatcher, error) {

	fmt.Println("exp: ", exp)

	secondMatcher, err := buildMatcher(exp[0], &minuteParser)
	if err != nil {
		return nil, err
	}
	minuteMatcher, err := buildMatcher(exp[1], &minuteParser)
	if err != nil {
		return nil, err
	}
	hourMatcher, err := buildMatcher(exp[2], &hourParser)
	if err != nil {
		return nil, err
	}
	dayMatcher, err := buildMatcher(exp[3], &dayParser)
	if err != nil {
		return nil, err
	}
	monthMatcher, err := buildMatcher(exp[4], &monthParser)
	if err != nil {
		return nil, err
	}
	weekMatcher, err := buildMatcher(exp[5], &weekParser)
	if err != nil {
		return nil, err
	}

	return &cronMatcher{secondMatcher, minuteMatcher, hourMatcher,
		dayMatcher, monthMatcher, weekMatcher}, nil
}

func buildMatcher(s string, p valueParser) (valueMatcher, error) {
	if s == "*" {
		return &alwaysTrueValueMatcher{}, nil
	}
	parts := split(s, ',')
	if len(parts) == 0 {
		return nil, errors.New("invalid field:" + s)
	}

	val := make([]int, 0)
	for _, part := range parts {
		locals, err := parseListElement(part, p)
		if err != nil {
			return nil, err
		}

		for _, local := range locals {
			find := false
			for _, v := range val {
				if v == local {
					find = true
				}
			}
			if !find {
				val = append(val, local)
			}
		}

	}
	return &intArrayValueMatcher{val}, nil

}

func parseListElement(s string, p valueParser) ([]int, error) {
	parts := split(s, '/')
	size := len(parts)
	if size < 1 || size > 2 {
		return nil, errors.New("syntax error")
	}
	val, err := parseRange(parts[0], p)
	if err != nil {
		return nil, err
	}
	if size == 1 {
		return val, nil
	}
	div, err := strconv.Atoi(parts[1])
	if err != nil {
		return nil, errors.New("invalid divisor")
	}
	if div < 1 {
		return nil, errors.New("non positive divisor ")
	}

	val2 := make([]int, 0)
	for i := 0; i < len(val); i += div {

		val2 = append(val2, val[i])
	}
	return val2, nil

}

func parseRange(s string, p valueParser) ([]int, error) {
	val := make([]int, 0)
	min, max := 0, 0

	switch v := p.(type) {
	case *simpleValueParser:
		min, max = v.min, v.max
	case *dayOfMonthValueParser:
		min, max = v.min, v.max
	case *monthValueParser:
		min, max = v.min, v.max
	case *weekValueParser:
		min, max = v.min, v.max
	}

	if s == "*" {
		for i := min; i <= max; i++ {
			val = append(val, i)
		}
		return val, nil
	}
	parts := split(s, '-')
	size := len(parts)
	if size < 1 || size > 2 {
		return nil, errors.New("syntax error")
	}
	v1, err := p.parse(parts[0])
	if err != nil {
		return nil, err
	}
	if size == 1 {
		val = append(val, v1)
		return val, nil
	}
	v2, err := p.parse(parts[1])
	if err != nil {
		return nil, err
	}
	if v1 < v2 {
		for i := v1; i <= v2; i++ {
			val = append(val, i)
		}
	} else if v1 > v2 {
		for i := v1; i <= max; i++ {
			val = append(val, i)
		}
		for i := min; i <= v2; i++ {
			val = append(val, i)
		}

	} else {
		val = append(val, v1)
	}
	return val, nil
}

type cronMatcher struct {
	secondMatcher valueMatcher
	minuteMatcher valueMatcher
	hourMatcher   valueMatcher
	dayMatcher    valueMatcher
	monthMatcher  valueMatcher
	weekMatcher   valueMatcher
}
type CronPattern struct {
	exp     string
	matcher []cronMatcher
}

func (c *CronPattern) Match(t time.Time) bool {

	second := t.Second()
	minute := t.Minute()
	hour := t.Hour()
	day := t.Day()
	month := int(t.Month())
	week := int(t.Weekday())
	year := t.Year()

	for _, m := range c.matcher {

		fmt.Println("second ", m.secondMatcher.match(second), m.secondMatcher)
		fmt.Println("minute ", m.minuteMatcher.match(minute), m.minuteMatcher)
		fmt.Println("hour ", m.hourMatcher.match(hour), m.hourMatcher)
		fmt.Println("day ", m.dayMatcher.match(day), m.dayMatcher)
		fmt.Println("month ", m.monthMatcher.match(month), m.monthMatcher)
		fmt.Println("week ", m.weekMatcher.match(week), m.weekMatcher)

		if m.secondMatcher.match(second) && m.minuteMatcher.match(minute) && m.hourMatcher.match(hour) &&
			(m.dayMatcher.match(day) || m.dayMatcher.match(32) && lastDayOfMonth(day, month, year)) &&
			m.monthMatcher.match(month) && m.weekMatcher.match(week) {
			return true
		}
	}
	return false
}

func parseAlias(val string, aliases []string, offset int) (int, error) {
	for idx, v := range aliases {

		if strings.EqualFold(val, v) {
			return idx + offset, nil
		}
	}
	return -1, errors.New("invalid alias:" + val)
}

func split(s string, ch rune) []string {

	return strings.FieldsFunc(s, func(c rune) bool {
		return c == ch
	})

}

func lastDayOfMonth(val int, mon int, year int) bool {

	if (year%4 == 0 && year%100 != 0 || year%400 == 0) && mon == 2 {
		return val == 29
	}
	return val == lastDays[mon-1]

}
